/*
 * Copyright (C) 2006 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.common.util.concurrent;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.Thread.currentThread;
import static java.util.Arrays.asList;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Ordering;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.j2objc.annotations.J2ObjCIncompatible;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;

/**
 * Static methods used to implement {@link Futures#getChecked(Future, Class)}.
 */
@GwtIncompatible
final class FuturesGetChecked {
    @CanIgnoreReturnValue
    static <V, X extends Exception> V getChecked(Future<V> future, Class<X> exceptionClass) throws X {
        return getChecked(bestGetCheckedTypeValidator(), future, exceptionClass);
    }

    /**
     * Implementation of {@link Futures#getChecked(Future, Class)}.
     */
    @CanIgnoreReturnValue
    @VisibleForTesting
    static <V, X extends Exception> V getChecked(GetCheckedTypeValidator validator, Future<V> future,
            Class<X> exceptionClass) throws X {
        validator.validateClass(exceptionClass);
        try {
            return future.get();
        } catch (InterruptedException e) {
            currentThread().interrupt();
            throw newWithCause(exceptionClass, e);
        } catch (ExecutionException e) {
            wrapAndThrowExceptionOrError(e.getCause(), exceptionClass);
            throw new AssertionError();
        }
    }

    /**
     * Implementation of {@link Futures#getChecked(Future, Class, long, TimeUnit)}.
     */
    @CanIgnoreReturnValue
    static <V, X extends Exception> V getChecked(Future<V> future, Class<X> exceptionClass, long timeout, TimeUnit unit)
            throws X {
        // TODO(cpovirk): benchmark a version of this method that accepts a GetCheckedTypeValidator
        bestGetCheckedTypeValidator().validateClass(exceptionClass);
        try {
            return future.get(timeout, unit);
        } catch (InterruptedException e) {
            currentThread().interrupt();
            throw newWithCause(exceptionClass, e);
        } catch (TimeoutException e) {
            throw newWithCause(exceptionClass, e);
        } catch (ExecutionException e) {
            wrapAndThrowExceptionOrError(e.getCause(), exceptionClass);
            throw new AssertionError();
        }
    }

    @VisibleForTesting
    interface GetCheckedTypeValidator {
        void validateClass(Class<? extends Exception> exceptionClass);
    }

    private static GetCheckedTypeValidator bestGetCheckedTypeValidator() {
        return GetCheckedTypeValidatorHolder.BEST_VALIDATOR;
    }

    @VisibleForTesting
    static GetCheckedTypeValidator weakSetValidator() {
        return GetCheckedTypeValidatorHolder.WeakSetValidator.INSTANCE;
    }

    @J2ObjCIncompatible // ClassValue
    @VisibleForTesting
    static GetCheckedTypeValidator classValueValidator() {
        return GetCheckedTypeValidatorHolder.ClassValueValidator.INSTANCE;
    }

    /**
     * Provides a check of whether an exception type is valid for use with
     * {@link FuturesGetChecked#getChecked(Future, Class)}, possibly using caching.
     *
     * <p>
     * Uses reflection to gracefully fall back to when certain implementations aren't available.
     */
    @VisibleForTesting
    static class GetCheckedTypeValidatorHolder {
        static final String CLASS_VALUE_VALIDATOR_NAME =
                GetCheckedTypeValidatorHolder.class.getName() + "$ClassValueValidator";

        static final GetCheckedTypeValidator BEST_VALIDATOR = getBestValidator();

        @IgnoreJRERequirement // getChecked falls back to another implementation if necessary
        @J2ObjCIncompatible // ClassValue
        enum ClassValueValidator implements GetCheckedTypeValidator {
            INSTANCE;

            /*
             * Static final fields are presumed to be fastest, based on our experience with
             * UnsignedBytesBenchmark. TODO(cpovirk): benchmark this
             */
            private static final ClassValue<Boolean> isValidClass = new ClassValue<Boolean>() {
                @Override
                protected Boolean computeValue(Class<?> type) {
                    checkExceptionClassValidity(type.asSubclass(Exception.class));
                    return true;
                }
            };

            @Override
            public void validateClass(Class<? extends Exception> exceptionClass) {
                isValidClass.get(exceptionClass); // throws if invalid; returns safely (and caches)
                                                  // if valid
            }
        }

        enum WeakSetValidator implements GetCheckedTypeValidator {
            INSTANCE;

            /*
             * Static final fields are presumed to be fastest, based on our experience with
             * UnsignedBytesBenchmark. TODO(cpovirk): benchmark this
             */
            /*
             * A CopyOnWriteArraySet<WeakReference> is faster than a newSetFromMap of a MapMaker map
             * with weakKeys() and concurrencyLevel(1), even up to at least 12 cached exception
             * types.
             */
            private static final Set<WeakReference<Class<? extends Exception>>> validClasses =
                    new CopyOnWriteArraySet<WeakReference<Class<? extends Exception>>>();

            @Override
            public void validateClass(Class<? extends Exception> exceptionClass) {
                for (WeakReference<Class<? extends Exception>> knownGood : validClasses) {
                    if (exceptionClass.equals(knownGood.get())) {
                        return;
                    }
                    // TODO(cpovirk): if reference has been cleared, remove it?
                }
                checkExceptionClassValidity(exceptionClass);

                /*
                 * It's very unlikely that any loaded Futures class will see getChecked called with
                 * more than a handful of exceptions. But it seems prudent to set a cap on how many
                 * we'll cache. This avoids out-of-control memory consumption, and it keeps the
                 * cache from growing so large that doing the lookup is noticeably slower than
                 * redoing the work would be.
                 *
                 * Ideally we'd have a real eviction policy, but until we see a problem in practice,
                 * I hope that this will suffice. I have not even benchmarked with different size
                 * limits.
                 */
                if (validClasses.size() > 1000) {
                    validClasses.clear();
                }

                validClasses.add(new WeakReference<Class<? extends Exception>>(exceptionClass));
            }
        }

        /**
         * Returns the ClassValue-using validator, or falls back to the "weak Set" implementation if
         * unable to do so.
         */
        static GetCheckedTypeValidator getBestValidator() {
            try {
                Class<?> theClass = Class.forName(CLASS_VALUE_VALIDATOR_NAME);
                return (GetCheckedTypeValidator) theClass.getEnumConstants()[0];
            } catch (Throwable t) { // ensure we really catch *everything*
                return weakSetValidator();
            }
        }
    }

    // TODO(cpovirk): change parameter order to match other helper methods (Class, Throwable)?
    private static <X extends Exception> void wrapAndThrowExceptionOrError(Throwable cause, Class<X> exceptionClass)
            throws X {
        if (cause instanceof Error) {
            throw new ExecutionError((Error) cause);
        }
        if (cause instanceof RuntimeException) {
            throw new UncheckedExecutionException(cause);
        }
        throw newWithCause(exceptionClass, cause);
    }

    /*
     * TODO(user): FutureChecker interface for these to be static methods on? If so, refer to it in
     * the (static-method) Futures.getChecked documentation
     */

    private static boolean hasConstructorUsableByGetChecked(Class<? extends Exception> exceptionClass) {
        try {
            Exception unused = newWithCause(exceptionClass, new Exception());
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private static <X extends Exception> X newWithCause(Class<X> exceptionClass, Throwable cause) {
        // getConstructors() guarantees this as long as we don't modify the array.
        @SuppressWarnings({"unchecked", "rawtypes"})
        List<Constructor<X>> constructors = (List) Arrays.asList(exceptionClass.getConstructors());
        for (Constructor<X> constructor : preferringStrings(constructors)) {
            @Nullable
            X instance = newFromConstructor(constructor, cause);
            if (instance != null) {
                if (instance.getCause() == null) {
                    instance.initCause(cause);
                }
                return instance;
            }
        }
        throw new IllegalArgumentException("No appropriate constructor for exception of type " + exceptionClass
                + " in response to chained exception", cause);
    }

    private static <X extends Exception> List<Constructor<X>> preferringStrings(List<Constructor<X>> constructors) {
        return WITH_STRING_PARAM_FIRST.sortedCopy(constructors);
    }

    private static final Ordering<Constructor<?>> WITH_STRING_PARAM_FIRST =
            Ordering.natural().onResultOf(new Function<Constructor<?>, Boolean>() {
                @Override
                public Boolean apply(Constructor<?> input) {
                    return asList(input.getParameterTypes()).contains(String.class);
                }
            }).reverse();

    @Nullable
    private static <X> X newFromConstructor(Constructor<X> constructor, Throwable cause) {
        Class<?>[] paramTypes = constructor.getParameterTypes();
        Object[] params = new Object[paramTypes.length];
        for (int i = 0; i < paramTypes.length; i++) {
            Class<?> paramType = paramTypes[i];
            if (paramType.equals(String.class)) {
                params[i] = cause.toString();
            } else if (paramType.equals(Throwable.class)) {
                params[i] = cause;
            } else {
                return null;
            }
        }
        try {
            return constructor.newInstance(params);
        } catch (IllegalArgumentException e) {
            return null;
        } catch (InstantiationException e) {
            return null;
        } catch (IllegalAccessException e) {
            return null;
        } catch (InvocationTargetException e) {
            return null;
        }
    }

    @VisibleForTesting
    static boolean isCheckedException(Class<? extends Exception> type) {
        return !RuntimeException.class.isAssignableFrom(type);
    }

    @VisibleForTesting
    static void checkExceptionClassValidity(Class<? extends Exception> exceptionClass) {
        checkArgument(isCheckedException(exceptionClass),
                "Futures.getChecked exception type (%s) must not be a RuntimeException", exceptionClass);
        checkArgument(hasConstructorUsableByGetChecked(exceptionClass),
                "Futures.getChecked exception type (%s) must be an accessible class with an accessible "
                        + "constructor whose parameters (if any) must be of type String and/or Throwable",
                exceptionClass);
    }

    private FuturesGetChecked() {}
}
